Skip to content

feat: add project concept for grouping generations (#65)#140

Merged
thedavidweng merged 4 commits into
mainfrom
feat/issue-65-projects
Jul 8, 2026
Merged

feat: add project concept for grouping generations (#65)#140
thedavidweng merged 4 commits into
mainfrom
feat/issue-65-projects

Conversation

@thedavidweng

@thedavidweng thedavidweng commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add projects table (migration 006) with project_id FK on generations (ON DELETE SET NULL)
  • Add backend DB methods: list_projects, create_project, rename_project, delete_project, set_generation_project, list_generations_by_project
  • Add Project / CreateProjectRequest / RenameProjectRequest models
  • Add Tauri commands: list_projects, create_project, rename_project, delete_project, assign_generation_to_project
  • Add CLI project subcommand (list, create, rename, delete, assign) with prefix/name resolution
  • Add --project flag to run (auto-creates project by name) and list (filter by project)
  • Add frontend Project type, API functions, projects store slice
  • Add ProjectSelector component in history sidebar with create/delete/filter
  • Add per-item project assignment dropdown in history items
  • Add i18n keys for projects.* in en.json and zh-CN.json
  • 3 new backend tests covering project CRUD, assignment, and cascade unassign

Test plan

  • cargo test — 20 cli_contract + 154 lib tests pass (3 new project tests)
  • npx tsc --noEmit — no type errors
  • npx vitest run — 957 tests pass
  • Manual: create project via CLI, run with --project, verify assignment
  • Manual: filter sidebar by project, assign/unassign from item dropdown

Generated with Devin


Open in Devin Review

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 18.88889% with 73 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.1%. Comparing base (1332810) to head (2422033).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/app/components/history/ProjectSelector.tsx 28.5% 27 Missing and 3 partials ⚠️
src/app/lib/store/slices/projects.ts 4.5% 21 Missing ⚠️
src/app/components/history/HistorySidebar.tsx 19.0% 15 Missing and 2 partials ⚠️
src/app/lib/api.ts 0.0% 5 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main    #140     +/-   ##
=======================================
- Coverage   72.1%   70.1%   -2.0%     
=======================================
  Files         61      63      +2     
  Lines       2392    2482     +90     
  Branches     710     729     +19     
=======================================
+ Hits        1725    1742     +17     
- Misses       476     544     +68     
- Partials     191     196      +5     
Flag Coverage Δ
frontend 70.1% <18.8%> (-2.0%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
frontend 70.1% <18.8%> (-2.0%) ⬇️
rust ∅ <ø> (∅)
Files with missing lines Coverage Δ
src/app/lib/store/slices/settings.ts 100.0% <ø> (ø)
src/app/lib/api.ts 72.2% <0.0%> (-5.4%) ⬇️
src/app/components/history/HistorySidebar.tsx 40.6% <19.0%> (-2.0%) ⬇️
src/app/lib/store/slices/projects.ts 4.5% <4.5%> (ø)
src/app/components/history/ProjectSelector.tsx 28.5% <28.5%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a "projects" concept that lets users group generations into named buckets, exposed through a new SQLite table, Tauri commands, CLI subcommand (project list/create/rename/delete/assign), a --project flag on run and list, and a ProjectSelector sidebar component with per-item assignment dropdowns.

  • Backend: migration 006 adds a projects table and a nullable project_id FK on generations (ON DELETE SET NULL); six new DB methods cover full CRUD and prefix-based resolution; find_generation_ids_by_prefix uses a targeted LIKE … LIMIT 2 query instead of loading all records.
  • Frontend: a new Zustand slice handles project state and optimistic updates; deleteProject correctly resets activeProjectId and clears projectId from in-memory history items; HistorySidebar filters the virtualised list client-side by activeProjectId.
  • CLI: project.rs fixes Windows CRLF trimming and shows the project name (not UUID) in the delete confirmation prompt; run.rs auto-creates a project by name when --project references an unknown name, without surfacing that creation to the user.

Confidence Score: 4/5

Safe to merge with one fix needed: the database connection must enable SQLite foreign key enforcement or the ON DELETE SET NULL cascade will never run.

The connection() method opens a fresh SQLite connection without calling PRAGMA foreign_keys = ON. SQLite disables foreign key enforcement by default, so the ON DELETE SET NULL declared in migration 006 is inert in production. After a project deletion and app restart, list_generations returns records that still carry stale project_id values referencing the now-deleted project. The in-session store update masks this for the current session, but the database-level state is wrong. The test delete_project_unassigns_generations_not_deletes_them also asserts project_id is null after deletion - an assertion that would fail without FK enforcement enabled.

src-tauri/src/services/db.rs — the connection() helper needs PRAGMA foreign_keys = ON added immediately after Connection::open; all other DB methods and the migration schema are correct once enforcement is enabled.

Important Files Changed

Filename Overview
src-tauri/migrations/006_add_projects.sql Adds projects table and project_id FK on generations with ON DELETE SET NULL — correct schema, but the cascade action relies on PRAGMA foreign_keys = ON which is never set.
src-tauri/src/services/db.rs Adds all project CRUD and generation-assignment DB methods; connection() never enables PRAGMA foreign_keys = ON, so the ON DELETE SET NULL declared in the migration is inert and the test verifying cascade unassign would fail without FK enforcement.
src-tauri/src/cli/project.rs New project CLI subcommand; delete confirmation now uses display_name (not UUID) and input.trim() (fixes Windows CRLF) addressing previous review issues.
src-tauri/src/cli/run.rs Adds --project flag with resolve_project_ref that auto-creates a project if no name or ID-prefix match is found, without notifying the user a new project was created.
src/app/lib/store/slices/projects.ts Clean Zustand slice for project state; deleteProject correctly resets activeProjectId and clears projectId from history items in local state.
src/app/components/history/HistorySidebar.tsx Adds project filter and per-item assignment dropdown; the assignment dropdown has no click-outside handler so it can be left open indefinitely.
src/app/components/history/ProjectSelector.tsx New sidebar component for listing, creating, and deleting projects; uses confirm() for delete which works correctly in Tauri's webview context.
src-tauri/src/commands/projects.rs Thin Tauri command wrappers for all project operations; straightforward delegation to DB methods.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as Frontend UI
    participant Store as Zustand Store
    participant Tauri as Tauri Commands
    participant DB as SQLite DB

    Note over UI,DB: Create Project
    UI->>Store: createProject(name)
    Store->>Tauri: create_project
    Tauri->>DB: INSERT INTO projects
    DB-->>Store: Project
    Store->>Store: prepend to projects[]

    Note over UI,DB: Delete Project
    UI->>Store: deleteProject(id)
    Store->>Tauri: delete_project(id)
    Tauri->>DB: "DELETE FROM projects WHERE id=?"
    DB-->>DB: "ON DELETE SET NULL (requires PRAGMA foreign_keys=ON)"
    Store->>Store: clear projectId in history[], reset activeProjectId

    Note over UI,DB: Filter History by Project
    UI->>Store: setActiveProject(id)
    Store->>UI: filteredHistory (client-side filter by projectId)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as Frontend UI
    participant Store as Zustand Store
    participant Tauri as Tauri Commands
    participant DB as SQLite DB

    Note over UI,DB: Create Project
    UI->>Store: createProject(name)
    Store->>Tauri: create_project
    Tauri->>DB: INSERT INTO projects
    DB-->>Store: Project
    Store->>Store: prepend to projects[]

    Note over UI,DB: Delete Project
    UI->>Store: deleteProject(id)
    Store->>Tauri: delete_project(id)
    Tauri->>DB: "DELETE FROM projects WHERE id=?"
    DB-->>DB: "ON DELETE SET NULL (requires PRAGMA foreign_keys=ON)"
    Store->>Store: clear projectId in history[], reset activeProjectId

    Note over UI,DB: Filter History by Project
    UI->>Store: setActiveProject(id)
    Store->>UI: filteredHistory (client-side filter by projectId)
Loading

Comments Outside Diff (1)

  1. src-tauri/src/services/db.rs, line 37-39 (link)

    P1 SQLite foreign key enforcement is never enabled

    PRAGMA foreign_keys = ON is absent from the entire codebase (grepped across all of src-tauri). SQLite disables foreign key enforcement by default, which means the ON DELETE SET NULL cascade declared in the migration is never executed. When a project is deleted via delete_project, the generations table retains stale project_id values pointing to the now-deleted project row.

    The in-session deleteProject store action manually clears projectId in frontend state, masking the bug during the current session. On the next app launch, list_generations will return these records with stale project_id references, and those generations will no longer appear under any project filter (since no project with that ID exists in state.projects). The test delete_project_unassigns_generations_not_deletes_them — which asserts fetched.project_id.is_none() after deletion — would also fail without FK enforcement enabled.

    Fix: call PRAGMA foreign_keys = ON in connection() immediately after opening the connection.

    Fix in Claude Code Fix in Codex Fix in Cursor

Fix All in Claude Code Fix All in Codex Fix All in Cursor

Reviews (3): Last reviewed commit: "fix: handle project assignment errors in..." | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

thedavidweng added a commit that referenced this pull request Jul 5, 2026
- rename_project preserves original created_at instead of overwriting
- delete confirmation uses trim() for cross-platform newline handling
- delete confirmation shows project name instead of UUID
- extract resolve_project_by_prefix to shared cli::mod (deduplicate)
- add find_generation_ids_by_prefix DB query for O(log n) prefix lookup

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
greptile-apps[bot]

This comment was marked as resolved.

thedavidweng and others added 4 commits July 7, 2026 21:45
Add a projects table (migration 006) with CRUD operations, CLI
`project` subcommand, `--project` flag on `run` and `list`, Tauri
commands, frontend store slice, ProjectSelector sidebar component,
and per-item project assignment dropdown.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- rename_project preserves original created_at instead of overwriting
- delete confirmation uses trim() for cross-platform newline handling
- delete confirmation shows project name instead of UUID
- extract resolve_project_by_prefix to shared cli::mod (deduplicate)
- add find_generation_ids_by_prefix DB query for O(log n) prefix lookup

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The unassign and assign onClick handlers fired
assignGenerationToProject without awaiting or catching, so a backend
failure produced an unhandled promise rejection and the UI silently
kept the stale project association. Attach .catch handlers that log
the failure, matching the existing warn-on-error pattern in this file.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Pick up the auto-format changes from the Dependabot merges and apply
rustfmt/oxfmt to the new project CLI and store slice code.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@thedavidweng thedavidweng force-pushed the feat/issue-65-projects branch from c22c70e to 2422033 Compare July 8, 2026 04:48

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +37 to +38
let name = if project.name.len() > 26 {
format!("{}…", &project.name[..25])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Truncating project names with non-ASCII characters crashes the CLI

A project name containing multi-byte characters (e.g., Chinese or emoji) is sliced at a raw byte offset (&project.name[..25] at src-tauri/src/cli/project.rs:38) rather than at a character boundary, so listing projects with long non-ASCII names causes a panic.

Impact: The openloop project list command crashes when any project has a long name containing multi-byte UTF-8 characters.

Byte-index slicing on user-provided UTF-8 strings

project.name.len() returns the byte length, and &project.name[..25] indexes into the string at byte position 25. For multi-byte UTF-8 characters (Chinese characters are 3 bytes, emoji are 4 bytes), byte position 25 can land in the middle of a character. Rust panics with byte index 25 is not a char boundary when this happens.

The app ships with a Chinese (zh-CN) locale (src/locales/zh-CN.json), so users are likely to create projects with Chinese names. A name like "这是一个很长的项目名称用来测试" (30 bytes for 10 characters) would trigger this crash.

The same pattern exists pre-PR for record.prompt[..21] at src-tauri/src/cli/list.rs:42, but that is pre-existing code not introduced by this PR.

Suggested change
let name = if project.name.len() > 26 {
format!("{}…", &project.name[..25])
let name = if project.name.chars().count() > 26 {
format!("{}…", project.name.chars().take(25).collect::<String>())
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
CREATE INDEX IF NOT EXISTS idx_projects_created_at ON projects(created_at DESC);

ALTER TABLE generations ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 SQLite foreign key ON DELETE SET NULL may not be enforced without PRAGMA

The migration at src-tauri/migrations/006_add_projects.sql:11 defines REFERENCES projects(id) ON DELETE SET NULL, and the test delete_project_unassigns_generations_not_deletes_them at src-tauri/src/services/db.rs:1036 asserts that deleting a project nulls out project_id on associated generations. However, SQLite does not enforce foreign key constraints by default — it requires PRAGMA foreign_keys = ON on each connection. No such pragma is set anywhere in the codebase (searched for foreign_keys with zero results). If the pragma is not enabled, deleting a project will leave orphaned project_id values in the generations table rather than setting them to NULL. The test may pass in certain SQLite builds or test environments but could fail or behave incorrectly in production. The Database::connection() method at src-tauri/src/services/db.rs:37 would be the right place to add this pragma.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@thedavidweng thedavidweng merged commit 6ea217a into main Jul 8, 2026
13 of 14 checks passed
@thedavidweng thedavidweng deleted the feat/issue-65-projects branch July 8, 2026 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant